import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.nio.ByteBuffer;

public class Client {
    private static final int PORT = 20000;
    private static final int LOCAL_PORT = 20001;

    public static void main(String[] args) throws IOException {
        Socket socket = createSocket();

        initSocket(socket);

        // 链接到本地20000端口，超时时间3秒，超过则抛出超时异常
        socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 3000);

        System.out.println("已发起服务器连接，并进入后续流程～");
        System.out.println("客户端信息：" + socket.getLocalAddress() + " P:" + socket.getLocalPort());
        System.out.println("服务器信息：" + socket.getInetAddress() + " P:" + socket.getPort());

        try {
            // 发送接收数据
            todo(socket);
        } catch (Exception e) {
            System.out.println("异常关闭");
        }

        // 释放资源
        socket.close();
        System.out.println("客户端已退出～");

    }

    private static Socket createSocket() throws IOException {
        /*
        // 无代理模式，等效于空构造函数
        Socket socket = new Socket(Proxy.NO_PROXY);

        // 新建一份具有HTTP代理的套接字，传输数据将通过www.baidu.com:8080端口转发
        // 这也是一个翻墙的原理，比如说我现在在香港的一个服务器上新建了一套代理的代码，然后我代理之后，比如代理端口是8800，
        // 然后我在内地的一个电脑上可以新建一个Socket，设置一份代理，代理的地址就是香港的服务器以及端口。我之后的Socket连接
        // 可以直接连接到美国的一台电脑上面去，而实际上并不是我的电脑直接连接到美国的电脑，而是我的电脑连接到香港的服务器，香港
        // 的服务器再去连接到美国的服务器，这就实现了一个数据的转发代理的一个模式。
        Proxy proxy = new Proxy(Proxy.Type.HTTP,
                new InetSocketAddress(Inet4Address.getByName("www.baidu.com"), 8800));
        socket = new Socket(proxy);

        // 新建一个套接字，并且直接链接到本地20000的服务器上
        socket = new Socket("localhost", PORT);

        // 新建一个套接字，并且直接链接到本地20000的服务器上
        socket = new Socket(Inet4Address.getLocalHost(), PORT);

        // 新建一个套接字，并且直接链接到本地20000的服务器上，并且绑定到本地20001端口上
        socket = new Socket("localhost", PORT, Inet4Address.getLocalHost(), LOCAL_PORT);
        socket = new Socket(Inet4Address.getLocalHost(), PORT, Inet4Address.getLocalHost(), LOCAL_PORT);
        */

        Socket socket = new Socket();
        // 绑定到本地20001端口
        socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), LOCAL_PORT));

        return socket;
    }

    private static void initSocket(Socket socket) throws SocketException {
        // SoTimeout并没有一个实际的概念，仅仅只是用于设置后面有阻塞的相关接口的超时时间，Socket当中有阻塞的有哪些东西呢？
        // 一个是连接，一个是读取数据。
        // 这里设置读取超时时间为2秒,
        socket.setSoTimeout(2000);

        // 是否复用未完全关闭的Socket地址，对于指定bind操作后的套接字有效
        // 一个Socket连接在建立连接之后，咋们首先要连接到远程IP+port，那么他要用的是什么？用的是咋们本地的一个端口号以及本地IP
        // 那么这个端口在用了只有，一般在2分钟之内是不允许再重新被分配使用的，如果我们设置了这个选项，那么这个端口号在连接完全
        // 关闭之后，那么就可以直接用来使用了。
        socket.setReuseAddress(true);

        // 是否开启Nagle算法，算法的意思是你的一个数据，咋们有说TCP的一个发送一份数据之后，会收到这份数据已接收到的回送，这是
        // 比较严谨的模式下，而在这个模式下，我们如果说发送了一个字节到服务器，服务器立刻又回送了一个ACK的确认包，而ACK的确认
        // 包往往是大于这一个字节的，他还带有一些其他信息。在这样的情况下，我们认为我们发送一个数据出去，回送的数据其实是消耗
        // 还比我们发送的实际数据还要多，我们比如发送10个字节，分成一个一个字节发送，然后我们会收到10份这样的回送，而这样的情
        // 况下其实是并不乐观的。那这种情况下往往是我发送一份数据过去，并不会立刻回送，可能会在第二份数据到来是，一起回送回来
        // 说前面两份都已经收到了，这取决于交互上的一个算法。
        // 那Nagle算法就是这样一个意思，在你的每一份数据发送之前，这一条数据在发送之后，我又有新的数据到达的话，我并不会把这份
        // 的数据直接发送出去，而是会进行等待，等待前面的一个数据回送回来之后，再把后面已经堆积到的所有数据一次性发送过去，那么
        // 就不是一个一个字节发送，有可能是这期间堆积了5个字节，那么将会把这5个字节一起发送，知道服务器端收到之后，再把这5个字
        // 节一起回送回来说收到了，这样子的算法有助于优化咋们网络之间的一个回送包的一个充斥的量，减少脏数据的可能性，优化网络空间
        socket.setTcpNoDelay(true);

        // 是否需要在长时无数据响应时发送确认数据（类似心跳包），时间大约为2小时
        // 当2小时之内，都没有发生数据交互的话，那么在你这个选项设置为true的情况下，将会发送一个类似心跳包的一个确认数据到
        // 服务器端，然后服务器端收到数据之后做一个回送，如果说这个期间回送失败了，那么我可以在客户端认为说这个连接已经断开了
        // 然后会做一些异常之类的抛出
        socket.setKeepAlive(true);

        // 对于close关闭操作行为进行怎样的处理；默认为false，0
        // false、0：默认情况，关闭时立即返回，底层系统接管输出流，将缓冲区内的数据发送完成
        // true、0：关闭时立即返回，缓冲区数据抛弃，直接发送RST结束命令到对方，并无需经过2MSL等待，MSL是网络当中数据包最长到达
        //      的理论时间值。
        // true、200：关闭时最长阻塞200毫秒，随后按第二情况处理

        socket.setSoLinger(true, 20);

        // 是否让紧急数据内敛，默认false；紧急数据就是通过 socket.sendUrgentData(1);发送的数据，他传入的是一个int值，其实取的
        // 是int值的低8位，把int值的低8位进行一个网络发送。紧急数据发送呢，默认的情况下你的客户端发送一份紧急数据到服务器端，服
        // 务器端收到之后经过一层过滤之后，对上层业务是无感知的，而如果你想要让业务有感知，那就是在接受方法之前，就是咋们Socket
        // 得到咋们的一个输入流，咋们输入流之前setOOBInline(true)，你在后面收到客户端发来的紧急数据的时候，将会紧急数据接受到
        // 咋们的一个逻辑层面，也就是咋们上层的一个程序层面。这个东西可以作为咋们的一个心跳包，但是一般情况下是不建议设置为true
        // 因为当你设置为true的情况下，你的紧急数据一旦有发送，可能会导致咋们的这个紧急数据被发送出去，而且跟咋们的行为数据混在
        // 了一起，可能会导致行为数据脏乱的问题。除非说你把这个UrgentData设置的值一定是在咋们后续的所有行为数据当中都不会出现的
        // 一个值，当然这种情况下非常小了，你的网络数据当中不可能说一定不出现某一个数据包吧。
        // true就是把紧急数据把他给内敛到咋们的一个行为数据当中。
        socket.setOOBInline(true);

        // 设置接收发送缓冲器大小，缓冲区大小并不是说一定等到这么大的缓冲区满了才发送咋们的数据，而是说当咋们设置发送的缓冲区为
        // 64K的时候，如果我发送的是32K的数据，那么32K数据会被直接发送出去；而我发送的是65K的数据，可能会把这个65K的数据拆分成
        // 64K+1K的数据进行发送，这个要拆分包，默认是32K的。
        socket.setReceiveBufferSize(64 * 1024 * 1024);
        socket.setSendBufferSize(64 * 1024 * 1024);

        // 设置性能参数：短链接，延迟，带宽的相对重要性。这3个值你设置多少并没有多大意义，你设置成100和设置成1是一模一样的
        // 他只是一个权重，代表3者的一个权重。
        // 连接时间是一个短连接时间，如果说你认为你这个Socket是一个短连接的，建立连接之后马上就要把他销毁掉，就是说你这个Socket
        // 建立连接的时间并不是很长，仅仅只是连接一会然后就要断开，在这种情况下，你认为短连接是重要的，你就把前面这个值相对于后
        // 面两个值提高你的权值。
        // 如果你认为你Socket对延迟是比较Cary的，你觉得你必须要保证你的延迟比较低，一个数据要马上及时的发送出去，那么你应该把
        // 延迟的权重提高。
        // 你的延迟一旦降低了，那么你的带宽必然就会受到阻碍，因为你的延迟，如果你要发送2个包，这2个包是分2次发呢，还是把两个包
        // 组合在一起发呢。如果2个包分别发，当你收到第一个包的时候立刻就把第一个包发送出去，这是比较及时的，延迟比较低。但如果
        // 说在收第一个包的时候可能要等个几毫秒或者10多毫秒，第二个包也来了，2个包一起组合发，那么这种情况下，这是要组包发送，
        // 组合发送呢，你的延迟自然而然是增高了，对于服务器而言你的第一个收到了一定的延迟，那你的带宽比较优秀了，带宽一次性就
        // 传送了2个数据包过去，这在传输当中称为带宽利用率比较高。带宽的利用率想要高就把带宽的权值调高。
        // 默认情况下3者是均衡的。比如说传送文件，那就把带宽的权重加高，因为传输文件需要比较长的时间，所以我们把连接也提高。
        // 如果你要及时发送数据就设置成1 1 0 这个时候连接时间比较低，延时也比较低。
        socket.setPerformancePreferences(1, 1, 0);
    }

    private static void todo(Socket client) throws IOException {
        // 得到Socket输出流
        OutputStream outputStream = client.getOutputStream();


        // 得到Socket输入流
        InputStream inputStream = client.getInputStream();
        byte[] buffer = new byte[256];
        ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);

        // byte
        byteBuffer.put((byte) 126);

        // char
        char c = 'a';
        byteBuffer.putChar(c);

        // int
        int i = 2323123;
        byteBuffer.putInt(i);

        // bool
        boolean b = true;
        byteBuffer.put(b ? (byte) 1 : (byte) 0);

        // Long
        long l = 298789739;
        byteBuffer.putLong(l);


        // float
        float f = 12.345f;
        byteBuffer.putFloat(f);


        // double
        double d = 13.31241248782973;
        byteBuffer.putDouble(d);

        // String
        String str = "Hello你好！";
        byteBuffer.put(str.getBytes());

        // 发送到服务器
        outputStream.write(buffer, 0, byteBuffer.position() + 1);

        // 接收服务器返回，read是收到的数据数量
        int read = inputStream.read(buffer);
        System.out.println("收到数量：" + read);

        // 资源释放
        outputStream.close();
        inputStream.close();
    }
}
